Skip to content

feat(token-fundraiser): add pinocchio example - #708

Open
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-fundraiser-pinocchio-revive
Open

feat(token-fundraiser): add pinocchio example#708
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-fundraiser-pinocchio-revive

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

What

Adds a Pinocchio implementation of the token-fundraiser example (the anchor example has no native sibling). A maker starts a fundraiser with a token target and a duration; contributors deposit into a PDA-owned vault up to a per-contributor cap while it runs. If the target is met the maker releases the funds; if the fundraiser ends without meeting it, contributors can refund.

This revives a previously-deferred port — its only blocker was that bankrun couldn't execute Clock::get(). The example now uses litesvm, which runs the Clock, so the time-based logic works and is testable.

Instructions

Four instructions over two PDA state accounts (fundraiser, contributor):

  1. initialize — create the fundraiser PDA + its vault ATA; record the target, duration, and start time.
  2. contribute — validate the per-contributor cap and that the fundraiser is still running; create the contributor record on first contribution; transfer tokens into the vault.
  3. check_contributions — once the target is met, release the vault to the maker (PDA-signed) and close the vault + fundraiser.
  4. refund — after the fundraiser ends without meeting the target, return a contributor's deposit (PDA-signed) and close their record.

Uses pinocchio-token / pinocchio-associated-token-account for the vault and transfers, invoke_signed for the PDA-authorized transfers, and the Clock sysvar for the time logic. The two inverted time checks in the anchor example are corrected here (contributions only while running; refunds only after the end).

Test

litesvm + @solana/kit, driving the full lifecycle by controlling the clock:

  • Refund path — contribute, warp the clock past the deadline, refund; assert the contributor is repaid and their account closed.
  • Release path — ten contributors reach the target (the 10% cap requires ten), then the maker releases the funds; assert the maker receives them and the fundraiser is closed.
Token Fundraiser (Pinocchio)
  ✔ Refunds a contributor after the fundraiser ends without meeting its target
  ✔ Releases the funds to the maker once the target is met
2 passing

Verified locally: cargo build-sbf, the litesvm tests, tsc --noEmit, Prettier, cargo fmt --check, Clippy, and pnpm install --frozen-lockfile all clean. (Deploy uses the *.so glob per #702.)


AI use: I directed the design (the instruction set, PDA/vault layout, the corrected time logic, the multi-scenario clock-driven test) and verified the token/PDA CPI patterns against the merged escrow pinocchio example; implementation and tests were written with Claude Code and reviewed by me.

Ports the token-fundraiser example to Pinocchio (the anchor example has no
native sibling). A maker starts a fundraiser with a token target and a
duration; contributors deposit tokens into a PDA-owned vault up to a
per-contributor cap while the fundraiser runs. Once the target is met the
maker releases the funds; if the fundraiser ends without meeting the target,
contributors can refund their deposits.

Four instructions (initialize, contribute, check_contributions, refund) over
two PDA state accounts, using pinocchio-token / pinocchio-associated-token-
account for the vault and transfers, PDA-signed CPIs to move funds out of the
vault, and the Clock sysvar for the time-based logic. The two inverted time
checks in the anchor example are corrected here (contributions are only
accepted while running; refunds only after the fundraiser ends).

The litesvm test drives the full lifecycle: the refund path (contribute, warp
the clock past the deadline, refund) and the release path (ten contributors
reach the target, then the maker releases the funds), controlling the clock to
exercise the time branches.
@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner August 30, 2026 09:40
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The follow-up changes complete the requested fundraiser hardening without leaving a blocking failure from the prior review.

  • Derives and validates the canonical signer-scoped contributor PDA before creating or loading its record.
  • Uses recorded campaign contributions rather than the externally mutable vault balance to determine release and refund eligibility.
  • Adds regression coverage for substituted contributor records, noncanonical records, and direct vault transfers.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported blocking failures are fixed and no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs Canonical contributor PDA validation now precedes both existing-account and account-creation paths, closing the previously reported identity and bump bypasses.
tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs Successful settlement now requires recorded contributions to reach the target while still sweeping the complete vault balance.
tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs Refund eligibility now depends on recorded campaign contributions, so unrecorded vault transfers cannot block contributors.
tokens/token-fundraiser/pinocchio/tests/test.ts Regression tests cover substituted and noncanonical contributor records plus direct vault deposits across release and refund behavior.

Reviews (4): Last reviewed commit: "token-fundraiser: apply prettier formatt..." | Re-trigger Greptile

…ded totals

contribute skipped the contributor PDA derivation whenever the supplied
record was already program-owned, so a signer could credit their transfer
to another participant's record: it bypassed the per-contributor cap and
left the tokens claimable by that record's owner through refund. The
derivation now runs before the branch, for existing and new records alike.

check_contributions and refund gated on the vault's token balance. The
vault is a standard ATA, so any holder could transfer straight into it and
push that balance past the target, releasing the fundraiser and blocking
legitimate refunds without any recorded contribution. Both now read
current_amount; the release still drains the full vault balance.

Adds LiteSVM coverage for both: a contribution into a substituted record is
rejected, and a direct vault transfer neither releases the fundraiser nor
prevents a refund. Both tests fail against the previous program.
contribute took the contributor bump from instruction data and fed it to
create_program_address. Several bumps can yield a valid address for the same
seeds, so a contributor could open extra, non-canonical records for
themselves and be metered against the per-contributor cap separately on each.
Those records were also unrefundable, since refund only ever derives the
canonical address.

The bump is now derived on-chain with find_program_address and the supplied
byte is gone from the instruction data entirely, so there is no longer a
caller-controlled input to the derivation. This matches the Anchor version,
whose `seeds = [...], bump` constraint likewise derives canonically rather
than accepting a bump from the client.

Adds a test that a contributor_account which is not the canonical PDA is
refused before any record is created.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant